﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace NUnit4PowerShell
{
    class PSUtils
    {
        /// <summary>
        /// Mock file overriding functions not handled by the PowerShell runspace instantiated
        /// </summary>
        private static readonly string MockScriptPath = Utils.GetMockCmdLetScriptPath();

        /// <summary>
        /// Prepare the PowerShell runspace for tests
        /// </summary>
        /// <returns>PowerShell runspace prepared</returns>
        internal static Runspace PSRunspaceSetup(string psFixture)
        {
            try
            {
                Runspace runspace = RunspaceFactory.CreateRunspace();
                runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;
                runspace.Open();

                string currentFolderPath = Directory.GetParent(psFixture).FullName;

                // We set the location to the current Scripts path locaton
                ExecuteCmdlet(string.Format("Set-Location -Path '{0}'", currentFolderPath), runspace);
                if (MockScriptPath != null && File.Exists(MockScriptPath))
                {
                    // We have to mock some functions as Write-Host
                    ExecuteCmdlet(string.Format(@"Import-Module '{0}' -force -DisableNameChecking", MockScriptPath), runspace);
                }
                // We import functions to test here
                ExecuteCmdlet(string.Format("Import-Module '{0}' -force -DisableNameChecking", psFixture), runspace);
                
                return runspace;
            }
            catch (Exception exception)
            {
                string fixtureName = Path.GetFileName(psFixture);
                Logger.Instance.Error("[PSRunspaceSetup]" + exception, fixtureName);
                throw;
            }
        }

        /// <summary>
        /// This method executes a PowerShell command in a runspace and return the result.
        /// </summary>
        /// <param name="command"></param>
        /// <param name="powershellRunspace"></param>
        /// <returns></returns>
        internal static Collection<PSObject> ExecuteCmdlet(string command, Runspace powershellRunspace)
        {
            try
            {
                using (Pipeline pipeline = powershellRunspace.CreatePipeline(command))
                {
                    try
                    {
                        //Logger.Instance.Info(string.Format("[ExecuteCmdlet] Execution of '{0}'", command));
                        Collection<PSObject> resultObject = pipeline.Invoke();
                        var errors = pipeline.Error.ReadToEnd();
                        if (errors.Count > 0)
                        {
                            string error =
                                ((System.Management.Automation.ErrorRecord)
                                 (((System.Management.Automation.PSObject)(errors[0])).ImmediateBaseObject)).
                                    FullyQualifiedErrorId;

                            var exception =
                                ((System.Management.Automation.ErrorRecord)
                                 (((System.Management.Automation.PSObject)(errors[0])).BaseObject)).Exception;

                            throw exception;
                            //TODO : Rename this exception with a dedicated exception
                            //TODO : Cast exception to not raise NUnit exception
                        }
                        return resultObject;
                    }
                    catch (Exception exception)
                    {
                        if (exception.InnerException != null &&
                            exception.InnerException.ToString().ToLower().Contains("nunit.framework"))
                        {
                            throw exception.InnerException;
                        }
                        throw;
                    }
                }
            }
            catch (System.Management.Automation.RuntimeException exception)
            {
                // Get the real reason of the exception : if not we do not get the cmdlet name responsible of the exception
                InvocationInfo errorInvocationInfo = exception.ErrorRecord.InvocationInfo;
                if (errorInvocationInfo == null)
                {
                    throw;
                }
                string error = CreateErrorMessage(errorInvocationInfo);
                throw new System.Management.Automation.CmdletProviderInvocationException(error, exception);
            }
            catch (System.Management.Automation.PSInvalidCastException exception)
            {
                // Get the real reason of the exception : if not we do not get the cmdlet name responsible of the exception
                InvocationInfo errorInvocationInfo = exception.ErrorRecord.InvocationInfo;
                string error = CreateErrorMessage(errorInvocationInfo);
                throw new System.Management.Automation.CmdletProviderInvocationException(error, exception);
            }
            catch (NUnit.Framework.AssertionException exception)
            {
                throw;
            }
            catch (NUnit.Framework.IgnoreException exception)
            {
                throw;
            }
            catch (NUnit.Framework.SuccessException exception)
            {
                throw;
            }
            catch (NUnit.Framework.InconclusiveException exception)
            {
                throw;
            }
        }

        /// <summary>
        /// This method functions following a name pattern
        /// </summary>
        /// <param name="pattern">Pattern used to find test functions</param>
        /// <returns>List of all function name which follow the pattern defined</returns>
        internal static List<string> FindPSTests(string pattern, Runspace runspace, string psFixtureFilePath)
        {

            // List of the functions to run
            List<string> functionsToTest = new List<string>();

            // Get all Powershell functions containing the test name pattern defined
            Collection<PSObject> resultPSObject = ExecuteCmdlet(string.Format("Get-Item function:{0}", pattern), runspace);

            // And add them in a list
            foreach (PSObject psObject in resultPSObject)
            {
                Logger.Instance.Info(string.Format("[FindPSTests] Functions added : {0}", psObject.ToString()), psFixtureFilePath);
                functionsToTest.Add(psObject.ToString());
            }

            return functionsToTest;
        }

        /// <summary>
        /// Methods which format a detailled message error 
        /// </summary>
        /// <param name="errorInvocationInfo"></param>
        /// <returns></returns>
        private static string CreateErrorMessage(InvocationInfo errorInvocationInfo)
        {
            string errorMessage = "";
            string errorLine = errorInvocationInfo.Line.Trim();
            int errorLineNumber = errorInvocationInfo.ScriptLineNumber;
            string scriptFullPath = errorInvocationInfo.ScriptName.Trim();
            string scriptName = Path.GetFileName(errorInvocationInfo.ScriptName);
            Dictionary<string, object> parameters = errorInvocationInfo.BoundParameters;

            // Get values of arguments
            string parameterString = "";
            foreach (KeyValuePair<string, object> parameter in parameters)
            {
                if (parameter.Value != null)
                {
                    string value = "";
                    if (parameter.Value is string[])
                    {
                        foreach (string aValue in (string[])parameter.Value)
                        {
                            value += string.Format("\"{0}\"", aValue);
                        }
                    }
                    else
                    {
                        value = string.Format("\"{0}\"", parameter.Value.ToString());
                    }
                    parameterString += string.Format("-{0} {1} ", parameter.Key, value);
                }
                else
                {
                    parameterString += string.Format("-{0} ", parameter.Key);
                }
            }

            // Build the message
            errorMessage = string.Format("{0}An error occured at line {1} in {2}", Environment.NewLine, errorLineNumber, scriptName);
            errorMessage += string.Format("{0} Command line generating an error is :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1}", Environment.NewLine, errorLine);
            errorMessage += string.Format("{0} Command line is :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1} {2}", Environment.NewLine, errorInvocationInfo.InvocationName, parameterString);
            errorMessage += string.Format("{0} In :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1}", Environment.NewLine, scriptFullPath);

            return errorMessage;
        }

        internal static bool ExecutePSFixtureFunction(string functionName, Runspace runspace, string fixtureFilePath)
        {
            Collection<PSObject> resultPSObject;
            try
            {
                resultPSObject = ExecuteCmdlet(string.Format("Get-Item function:{0}", functionName), runspace);
            }
            catch (ItemNotFoundException)
            {
                return false;
            }
            if (resultPSObject.Count == 1)
            {
                return true;
            }
            else
            {
                throw new Exception(string.Format("Several functions {0} have been found in {1}", functionName, fixtureFilePath));
            }
        }
    }
}
